Sealed Interface 是 Java 17 引入的新的特性,而 Kotlin 也滿早就有這樣的功能,今天來討論如何利用 sealed interface 作領域建模。
假設我們有一個電影分級制度欄位 AgeRestriction,一般可能會用 String 欄位,那我們可以用 enum 進一步來建模
enum class AgeRestriction(val description: String) {
General("普遍级"),
Protect("保護級"),
PG15("輔導十五歲,未滿十五歲之人不得觀賞"),
PG12("輔導十二歲,未滿十二歲之兒童不得觀賞"),
Restricted("限制級")
}
使用 enum class 比使用 String 強大得多。String 有無窮的可能值,但現在我們只有五個可能的值。因此,使用 AgeRestriction 比使用 String 更容易理解和操作。
在函數式程式設計中,這種資料組合也被稱為sum type,其資料模型為"或"關係。所以我們可以說 AgeRestriction 是有限的 1+1+1+1+1 的可能性。這比 String 告訴我們更多。String會有無窮的值,而以 enum class 建模的 AgeRestriction 只有五種不同的值。因此,使用sum type可以大大減少複雜性, 並且增加可能性。
假設要對一個電影上映進行建模,隨著網路影片平台的興起,會有一種上映事件不是發生在戲院的位址上。而是在某個 Url 上發生的活動。因此,這個 Event 的類型,可能可以這樣簡單地實現它:
@JvmInline value class Url(val value: String)
@JvmInline value class City(val value: String)
@JvmInline value class Street(val value: String)
data class Address(val city: City, val street: Street)
data class Event(
val id: EventId,
val title: String,
val organizer: String,
val description: String,
val date: LocalDate,
val ageRestriction: AgeRestriction,
val isOnline: Boolean,
val url: Url?,
val address: Address?
)
這是一種常見的開發方式,但它可能會有問題。如果isOnline為true,url將為非null,反之亦然。但在檢查isOnline後,url和address仍然是null,所以我們最後得到的代碼像這樣。
fun printLocation(event: Event): Unit =
if(event.isOnline) {
event.url?.value?.let(::println)
} else {
event.address?.let(::println)
}
但更糟糕的是,我們也可以輕易地破壞既定的合約,就像下面的例子那樣。
Event(
Id(0L),
"Functional Domain Modeling",
"47 Degrees",
"Building software with functional DDD...",
LocalDate.now(),
AgeRestriction.General,
true,
null,
null
)
因為這樣的型別模型無法限制這樣可能性。即使我們透過 function 說,如果它是 isOnline,那麼 url 將是非null。
這時就可以通過引入 sealed class,將 Event.Online 和 Event.AtAddress 以型別化的方式組合在一起,以防止這個問題。
sealed class Event {
abstract val id: EventId
abstract val title: String
abstract val organizer: String
abstract val description: String
abstract val ageRestriction: AgeRestriction
abstract val date: LocalDate
data class Online(
override val id: EventId,
override val title: String,
override val organizer: String,
override val description: String,
override val ageRestriction: AgeRestriction,
override val date: LocalDate,
val url: Url
) : Event()
data class AtAddress(
override val id: EventId,
override val title: String,
override val organizer: String,
override val description: String,
override val ageRestriction: AgeRestriction,
override val date: LocalDate,
val address: Address
) : Event()
}
這解決了先前的問題,可以建立一個沒有Url的線上Event,並提供了一個更好的型別處理方式。現在,我們可以使用精確的方式來比對、處理 Event,由於Kotlin的型別推斷,我們可以安全地在 Event.Online 的情況下訪問 url。
fun printLocation(event: Event): Unit =
when(event) {
is Online -> println(event.url.value)
is AtAddress -> println("${event.address.city}: ${event.address.street}")
}
這種型別組成方式也被稱為sum type,其模型為"或"關係,但 sealed class 比 enum class提供更強大的功能。sealed class 允許情況存在於物件、數據類或甚至另一個sealed class。enum class不能
sealed class 在資料建模時提供了很大型別保證。但是還是要考慮使用的場景,如有 Json 轉換的需要,會需要根據 library 的方式建立特定的 serializer, 如 Jackson。